﻿"""
cyserver.pyx

This file is part of a program that implements a Software-Defined Radio.

Copyright (C) 2014 by G3UKB Bob Cowdery

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

The authors can be reached by email at:

	bob@bobcowdery.plus.com

"""

# Python imports
import sys, os
import threading
import socket
import select
import struct
import queue
from ctypes import *
from time import sleep
import numpy as np
import logging

# Cython imports
cimport numpy as np
np.import_array()

# Application imports
from common.defs import *

#=================================================================================
# C declarations
cdef extern from "string.h" nogil:
	char *strcpy  (char *TO, char *FROM)
	char *strncpy (char *TO, char *FROM, size_t SIZE)
cdef extern from "utils.h":
	void c_set_freq(double freq, char *ptr_bytes)
cdef extern from "local_audio.h":
	ctypedef struct DeviceEnum:
		int direction
		int index
		char name[50]
		int channels
		char host_api[50]
	ctypedef struct DeviceEnumList:
		int entries
		DeviceEnum devices[50]
	DeviceEnumList* enum_inputs()
	DeviceEnumList* enum_outputs()
cdef extern from "server.h":
	ctypedef struct Tuple:
		int inst_id
		int ch_id
	ctypedef struct General:
		int in_rate
		int out_rate
		int iq_blk_sz
		int mic_blk_sz
		int duplex
	ctypedef struct Route:
		int rx
		char hostapi[50]
		char dev[50]
		char ch[10]
	ctypedef struct Routes:
		Route hpsdr[2]
		Route local[8]
	ctypedef struct Audio:
		char in_src[30]
		char in_hostapi[50]
		char in_dev[50]
		char out_sink[30]
		Routes routing
	ctypedef struct Args:
		# TX/RX allocations
		int num_tx
		Tuple tx[1]
		int num_rx
		Tuple rx[8]
		Tuple disp[8]	
		# Configuration
		General general
		Audio audio
		
	int c_server_init(Args args)
	int c_server_start()
	int c_server_run_display(int display_run)
	int c_server_terminate()
	int c_server_open_channel(int ch_type, int ch_instance, int iq_size, int mic_size, int in_rate, int out_rate, int tdelayup, int tslewup, int tdelaydown, int tslewdown)
	void c_server_close_channel(int channel)
	void c_server_set_input_samplerate(int channel, int rate)
	void c_server_set_ch_state(int channel, int state, int mode)
	void c_server_set_dsp_sz(int channel, int sz)
	void c_server_set_tdelayup(int channel, int delay)
	void c_server_set_tslewup(int channel, int slew)
	void c_server_set_tdelaydown(int channel, int delay)
	void c_server_set_tslewdown(int channel, int slew)
	void c_server_make_wisdom(char *dir)
	void c_server_set_rx_mode(int channel, int mode)
	void c_server_set_rx_filter_run(int channel, int run)
	void c_server_set_rx_filter_freq(int channel, int low, int high)
	void c_server_set_rx_filter_window(int channel, int window)
	void c_server_set_agc_mode(int channel, int mode)
	double c_server_get_rx_meter_data(int channel, int which)
	void c_server_set_rx_gain(int rx, float gain)
	void c_server_set_tx_mode(int channel, int mode)
	void c_server_set_tx_filter_run(int channel, int run)
	void c_server_set_tx_filter_freq(int channel, int low, int high)
	void c_server_set_tx_filter_window(int channel, int window)
	void c_server_set_tx_gain(float gain)
	double c_server_get_tx_meter_data(int channel, int which)
	int c_server_open_display(int fft_size, int win_type, int sub_spans, int in_sz, int display_width, int average_mode, int over_frames, int sample_rate, int frame_rate)
	void c_server_set_display(int display_id, int fft_size, int win_type, int sub_spans, int in_sz, int display_width, int average_mode, int over_frames, int sample_rate, int frame_rate)
	void c_server_close_display(int display_id)
	int c_server_get_display_data(int display_id, void *display_data)
	
#=================================================================================

# Lock for cc data
lock = threading.RLock()
message_q = None

"""
Server class:
	Start Metis reader/writer threads
	Implementation of client API calls.

"""
class Server():
	
	def __init__(self, metis_sock, metis_address, METIS_PORT, q):
		"""
		Initialise processing
		
		Arguments:
			metis_sock			-- open socket to Metis hardware
			metis_address		-- IP address of Metis hardware
			METIS_PORT			-- Port Metis listens on
			q					-- Q to send messages on
			
		"""
		global message_q
		
		self.__metis_sock = metis_sock
		self.__metis_address = metis_address
		self.__METIS_PORT = METIS_PORT
		self.__q = q
		message_q = q
		
		# Create the cc array
		# There are 0-10 valid values of C0 and each cc sequence is 5 bytes
		self.__cc = np.zeros((11,5), dtype=np.uint8)
		self.__init_cc()
		
		# State
		self.__num_rx = 1
		self.__duplex = False
		self.__display_poller_list = {}
		
		# Class vars
		self.__frame_rate = 10 # default
	
	# =========================================================================================================	
	# API
	# =========================================================================================================
	
	# =========================================================================================================	
	# Audio device enumeration
	def enum_inputs(self):
		""" Enum compatible input devices """
		
		devs = enum_inputs()
		return __convert_device_list(devs)
	
	def enum_outputs(self):
		""" Enum compatible output devices """
		
		devs = enum_outputs()
		return __convert_device_list(devs)
		
	# =========================================================================================================	
	# Management - start/stop/reset processing
	def start_processing(self, control_cfg, tx_chs, rx_chs, rx_disp, cc_callback, test_mode):
		"""
		Start the processing
		
		Arguments:
			control_cfg			-- the current configuration as held by the control ui
			tx_chs				-- ((tx_id, ch), ...)
			rx_chs				-- ((rx_id, ch), ...)
			rx_disp				-- (rx_id, unit), ...)
			cc_callback			-- Callback here with cc data
			test_mode			-- Don't start/stop the back-end as we are developing the UI
			
		Configuration is of the form:	
		DEFAULT_CONFIG: {
				COMMON: {
					IN_RATE: 48000,
					OUT_RATE: 48000,
					NUM_RX: 1,
					IQ_BLK_SZ: 1024,
					MIC_BLK_SZ: 1024,
					DUPLEX: False,
					TX: False,
					POWER: {
						PWR_MODE: AUTO,
						PWR_IP: '192.168.1.100',
						PWR_USER: 'admin',
						PWR_PW: '12345678',
						PWR_OUTLET: 1,
					},
					AUDIO: {
						IN_SRC: HPSDR,
						IN_HOSTAPI: '',
						IN_DEV: '',
						OUT_SINK: HPSDR,
						ROUTING: {
							HPSDR:[[1, '', '', BOTH],],
							LOCAL: []   
						}
					},
					ANTENNA: {
						ANT_SETTING: {ANT_1: [MIN_FREQ, MAX_FREQ],},
						ANT_PREC: 'default'
					}
				},
				RX: {
					DISPLAYS: {
						PAN_MODE: 3,
						PAN_FRM_AV: 30,
						WINDOW_TYPE: 5,
						PAN_FFT_SZ: 4096
					}
				}
				TX: {
					LEVELS: {
						TUNE_DRIVE_LEVEL: 20
					}
				} 
			}
			
		"""

		self.__test_mode = test_mode
		
		# Create the processor instance to implement the line protocol
		self.__processor = Processor(control_cfg, self.__metis_sock, self.__metis_address, self.__METIS_PORT, self.__cc, cc_callback, self.__q)
		
		# Initialise the pipeline
		# Create and populate the args structure
		# This is our interface to the C world that informs it of:
		# 	all pre_allocated tx and rx channels
		#	current configuration values for globals and audio
		cdef Args args
		args.num_tx = len(tx_chs)
		args.num_rx = len(rx_chs)
		for ch in range(len(tx_chs)):
			args.tx[ch].inst_id = tx_chs[ch][0]
			args.tx[ch].ch_id = tx_chs[ch][1]
		for ch in range(len(rx_chs)):
			args.rx[ch].inst_id = rx_chs[ch][0]
			args.rx[ch].ch_id = rx_chs[ch][1]
		for unit in range(len(rx_disp)):
			args.disp[unit].inst_id = rx_disp[unit][0]
			args.disp[unit].ch_id = rx_disp[unit][1]
		# Global config
		args.general.in_rate = control_cfg[COMMON][IN_RATE]
		args.general.out_rate = control_cfg[COMMON][OUT_RATE]
		args.general.iq_blk_sz = control_cfg[COMMON][IQ_BLK_SZ]
		args.general.mic_blk_sz = control_cfg[COMMON][MIC_BLK_SZ]
		args.general.duplex = control_cfg[COMMON][DUPLEX]
		# Audio config
		# Note: Cython tries to protect from variables going out of scope.
		# The act of saying .encode("ascii","ignore") creates a temporary
		# python string. The compiler will not let this be used as a char*
		# (Obtaining 'char *' from temporary Python value) so it must be copied
		# into a new value first. As we were copying it anyway in strcpy() there
		# was no danger but the compiler isn't that smart.
		s = control_cfg[COMMON][AUDIO][IN_SRC].encode("ascii","ignore")
		strcpy(args.audio.in_src, s)
		s = control_cfg[COMMON][AUDIO][IN_HOSTAPI].encode("ascii","ignore")
		strcpy(args.audio.in_hostapi, s)
		s = control_cfg[COMMON][AUDIO][IN_DEV].encode("ascii","ignore")
		strcpy(args.audio.in_dev, s)
		s = control_cfg[COMMON][AUDIO][OUT_SINK].encode("ascii","ignore")
		strcpy(args.audio.out_sink, s)
		n = 0
		for i in range(2):
			args.audio.routing.hpsdr[i].rx = -1		
		for route in control_cfg[COMMON][AUDIO][ROUTING][HPSDR]:
			args.audio.routing.hpsdr[n].rx = route[0]
			s = route[1].encode("ascii","ignore")
			strcpy(args.audio.routing.hpsdr[n].hostapi, s)
			s = route[2].encode("ascii","ignore")
			strcpy(args.audio.routing.hpsdr[n].dev, s)
			s = route[3].encode("ascii","ignore")
			strcpy(args.audio.routing.hpsdr[n].ch, s)
			n += 1
		n = 0
		for i in range(MAX_RX):
			args.audio.routing.local[i].rx = -1
		for route in control_cfg[COMMON][AUDIO][ROUTING][LOCAL]:
			args.audio.routing.local[n].rx = route[0]
			s = route[1].encode("ascii","ignore")
			strcpy(args.audio.routing.local[n].hostapi, s)
			s = route[2].encode("ascii","ignore")
			strcpy(args.audio.routing.local[n].dev, s)
			s = route[3].encode("ascii","ignore")
			strcpy(args.audio.routing.local[n].ch, s)
			n += 1
				
		# Initialise the C level server
		if not c_server_init(args):
				# Something failed
				return False
	
		# Do any local initialisation of HPSDR
		# This will override defaults
		self.set_speed(control_cfg[COMMON][IN_RATE])
		self.set_duplex(control_cfg[COMMON][DUPLEX])
		self.set_number_rx(control_cfg[COMMON][NUM_RX])
		
		# Start the processing
		if not test_mode:
				c_server_start()
				self.__processor.start()
				
		return True
	
	def run_display(self, run_state):
		""" Toggle the run state of the display processing in the pipeline """
		
		c_server_run_display(run_state)
		
	def stop_processing(self):
		""" Stop the processing """
		
		if not self.__test_mode:
			self.__processor.terminate()
			c_server_terminate()
	
	# ========================================================================================================
	# FFTW wisdom (tune FFTW)
	def make_wisdom(self):
		"""
		Make a wisdom file if it does not exist.
		The system will choose the location.
		
		"""	
		
		# Python 3 uses Unicode for strings by default, either UTF16 or UTF32 depending on narrow or wide build
		# This needs to be a byte array to pass into C functions that require char* - hence the encoding
		# No other trick is necessary to call C functions requiring a char*.
		# Note, the final slash is required as the WDSP level appears to expect it.
		if not os.path.exists(os.path.join('..','..','wisdom')):
			os.mkdir(os.path.join('..','..','wisdom'))
		path = os.path.join('..','..','wisdom', '').encode('ascii', 'ignore')
		self.__q.put ((BOTH, logging.INFO, 'cy.cyserver', 'Starting FFTW Wisdom'))
		c_server_make_wisdom(path)
		self.__q.put ((BOTH, logging.INFO, 'cy.cyserver', 'FFTW Wisdom complete'))
	
	# =========================================================================================================	
	# HPSDR H/W - The H/W is addressed by in-band signalling. A sequence of 5 bytes is sent with each
	# with each frame. The meaning of the sequence is determined by the value of byte 0. The separate
	# sequences are sent round-robin.
	
	def set_mox(self, value):
		"""
		Set MOX on/off
		
		Arguments:
			value -- True | False
		
		"""
		
		for n in range(11):
			self.__set_bits(value, n, 0, 0xFE, {True:0x01, False:0x00})		

	def set_speed(self, value):
		"""
		Set the input sample rate
		
		Arguments:
			value -- integer speed value
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 1, 0xFC, {48000:0x00, 96000:0x01, 192000:0x02, 384000:0x03})
	
	def set_10MHz_ref(self, value):
		"""
		Set the 10Mhz reference source
		
		Arguments:
			value -- name of source
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 1, 0xF3, {'external':0x00, 'penelope':0x04, 'mercury':0x08})
	
	def set_122_88MHz_ref(self, value):
		"""
		Set the 122.88Mhz reference source
		
		Arguments:
			value -- name of source
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 1, 0xEF, {'penelope':0x00, 'mercury':0x10})
	
	def set_board_config(self, value):
		"""
		Set the board configuration
		
		Arguments:
			value -- board mix
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 1, 0x9F, {'nil':0x00, 'penelope':0x20, 'mercury':0x40, 'both':0x60})
	
	def set_mic_boost(self, value):
		""" Set the mic boost
		
		Arguments:
			value -- False-0dB, True=20dB
		
		"""
		
		self.__set_bits(value, CC_OUT_AUX_1, 2, 0xFE, {True:0x01, False:0x00})
	
	def set_mic_line(self, value):
		""" Set the mic/line 
		
		Arguments:
			value -- 'mic', 'line'
		
		"""
		
		self.__set_bits(value, CC_OUT_AUX_1, 2, 0xFD, {'mic':0x00, 'line':0x02})
		
	def set_mic_source(self, value):
		""" Set the mic source
		
		Arguments:
			value -- microphone source
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 1, 0x7F, {'janus':0x00, 'penelope':0x80})

	def set_class_mode(self, value):
		"""
		Set the class mode
		
		Arguments:
			value -- E or other
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 2, 0xFE, {'mode_e':0x00, 'mode_other':0x01})
	
	def set_oc_outputs(self, value):
		"""
		Set the open collector outputs
		
		Arguments:
			value -- bit settings
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 2, 0x01, {value:value<<1})
	
	def set_alex_attn(self, value):
		"""
		Set Alex attenuator level
		
		Arguments:
			value -- db level
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 3, 0xFC, {0:0x00, 10:0x01, 20:0x02, 30:0x03})
	
	def set_preamp(self, value):
		"""
		Set Alex preamp
		
		Arguments:
			value -- True = on
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 3, 0xFB, {True:0x04, False:0x00})
		
	def set_LT2208_dither(self, value):
		"""
		Set L2280 Dither
		
		Arguments:
			value -- True = on
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 3, 0xF7, {True:0x08, False:0x00})

	def set_LT2208_random(self, value):
		"""
		Set L2280 Random
		
		Arguments:
			value -- True = on
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 3, 0xEF, {True:0x10, False:0x00})
		
	def set_alex_rx_ant(self, value):
		"""
		Select Alex rx antenna source
		
		Arguments:
			value -- antenna source
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 3, 0x9F, {'none':0x00, 'rx1':0x20, 'rx2':0x40, 'xv':0x60})
		
	def set_alex_rx_out(self, value):
		"""
		Set Alex RX out
		
		Arguments:
			value -- True = on
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 3, 0x7F, {True:0x80, False:0x00})
		
	def set_alex_tx_relay(self, value):
		"""
		Set Alex TX relay output
		
		Arguments:
			value -- relay output
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 4, 0xFC, {'tx1':0x00, 'tx2':0x01, 'tx3':0x10})

	def set_alex_6m_preamp(self, value):
		"""
		Set the 6M preamp state
		
		Arguments:
			value -- True = on
		
		"""
		
		self.__set_bits(value, CC_OUT_AUX_1, 3, 0xBF, {True:0x40, False:0x00})
		
	def set_duplex(self, value):
		"""
		Set simplex/duplex
		
		Arguments:
			value -- True = duplex
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 4, 0xFB, {True:0x04, False:0x00})
		self.__duplex = value
		
	def set_number_rx(self, value):
		"""
		Set number of receivers
		
		Arguments:
			value -- number 1-8
		
		"""

		self.__set_bits(value, CC_OUT_GEN, 4, 0xC7, {1:0x00, 2:0x08, 3:0x10, 4:0x18, 5:0x20, 6:0x28, 7:0x30, 8:0x38})
		self.__num_rx =  value
		
	def set_mic_ts(self, value):
		"""
		Set 1PPS timestamp on ic data
		
		Arguments:
			value -- True = on
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 4, 0xBF, {True:0x40, False:0x00})
	
	def set_mercury_freq_plan(self, value):
		"""
		Set independent or common frequency for multiple mercury boards
		
		Arguments:
			value -- plan
		
		"""
		
		self.__set_bits(value, CC_OUT_GEN, 4, 0x7F, {'independent':0x00, 'common':0x80})
	
	def set_rx_freq(self, rx, freq):
		"""
		Set frequency for rx
		
		Arguments:
			rx 		-- rx number (1-8)
			freq 	-- float format in MHz
		
		"""

		# NCO_1 is the RX and TX frequency when a single receiver is running in simplex mode.
		# NCO-1 is the TX only frequency when running in duplex mode.
		# NC0-2 to NCO-8 are the RX frequencies for RX 1-7 when running multiple receivers.
		# NCO-1 and NC0-2 need to be set for a single RX in simplex
		cdef char * nco_ptr1 = np.PyArray_BYTES(self.__cc[CC_OUT_NCO_1])
		cdef char * nco_ptr2 = np.PyArray_BYTES(self.__cc[CC_OUT_NCO_1 + rx])
		if self.__num_rx == 1 and not self.__duplex:
			lock.acquire()
			c_set_freq(freq, nco_ptr1)
			c_set_freq(freq, nco_ptr2)
			lock.release()
		else:			
			lock.acquire()			
			c_set_freq(freq, nco_ptr2)
			lock.release()
			
	def set_tx_freq(self, tx, freq):
		"""
		Set frequency for tx
		
		Arguments:
			tx 		-- tx number (1)
			freq 	-- float format in MHz
		
		"""

		# NCO_1 is the RX and TX frequency when a single receiver is running in simplex mode.
		# NCO-1 is the TX only frequency when running in duplex mode.
		cdef char * nco_ptr1 = np.PyArray_BYTES(self.__cc[CC_OUT_NCO_1])		
		lock.acquire()
		c_set_freq(freq, nco_ptr1)
		lock.release()
		
	# =========================================================================================================	
	# DSP
	
	#======================================================
	# Basic channel operations
	def open_channel(self, ch_type, ch_instance, iq_size, mic_size, in_rate, out_rate, tdelayup, tslewup, tdelaydown, tslewdown):
		"""
		Open a new DSP channel 
		
		Arguments:
			ch_type 	-- CH_RX | CH_TX
			ch_instance	-- Rx/TX id for this channel
			iq_size		-- 128, 256, 1024, 2048, 4096
			mic_size	-- same as iq_size for same sample rate
			in_rate		-- input sample rate
			out_rate	-- output sample rate
			tdelayup	-- delay before up slew
			tslewup		-- length of up slew
			tdelaydown	-- delay before down slew
			tslewdown	-- length of down slew
			
		Returns:
			the assigned channel number
		
		The channel is not automatically started. Call set_ch_state() to start the channel.
			
		"""
		
		return c_server_open_channel(ch_type, ch_instance, iq_size, mic_size, in_rate, out_rate, tdelayup, tslewup, tdelaydown, tslewdown)
	
	def close_channel(self, channel):
		"""
		Close an open channel 
		
		Arguments:
			channel 	-- the channel id as returned by open_channel()
			
		"""
		
		c_server_close_channel(channel)
	
	def set_ch_state(self, channel, state, mode):
		"""
		Change the channel state 
		
		Arguments:
			channel 	-- the channel id as returned by open_channel()
			state		-- CH_STATE_STOP | CH_STATE_START
			mode		-- CH_TRANSITION_WAIT | CH_TRANSITION_NOWAIT
			
		"""
		
		c_server_set_ch_state(channel, state, mode)
	
	def set_input_samplerate(self, channel, rate):
			"""
			Set the input sample rate 
			
			Arguments:
				channel	-- the channel id as returned by open_channel()
				rate 	-- rate in Hz (48000 | 96000 | 192000 | 384000)
			
			"""
			
			c_server_set_input_samplerate(channel, rate)
		
	def set_dsp_sz(self, channel, sz):
		"""
		Set the DSP block size in complex samples 
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			sz 		-- block size
		
		"""
		
		c_server_set_dsp_szz(channel, sz)
		
	def set_tdelayup(self, channel, delay):
		"""
		Change the delay up time in seconds 
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			delay 	-- delay in seconds
		
		"""
		
		c_server_set_tdelayup(channel, delay)
		
	def set_tslewup(self, channel, slew):
		"""
		Change the slew up time in seconds 
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			slew 	-- slew up time in seconds
		
		"""
		
		c_server_set_tslewup(channel, slew)
		
	def set_tdelaydown(self, channel, delay):
		"""
		Change the delay up time in seconds 
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			delay 	-- delay in seconds
		
		"""
		
		c_server_set_tdelaydown(channel, delay)
		
	def set_tslewdown(self, channel, slew):
		"""
		Change the slew down time in seconds 
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			slew 	-- slew down time in seconds
		
		"""
		
		c_server_set_tslewdown(channel, slew)
	
	#======================================================
	# Receiver type channel parameters
	def set_rx_mode(self, channel, mode):
		
		"""
		Set the receiver mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			mode 	-- CH_LSB | CH_USB |CH_DSB |CH_CWL |CH_CWU |CH_FM |CH_AM |CH_DIGU |CH_SPEC |CH_DIGL |CH_SAM | CH_DRM
		
		"""
		
		c_server_set_rx_mode(channel, mode)
	
	def set_rx_filter_run(self, channel, run):
		
		"""
		Set the receiver bandpass run mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			run  	-- CH_BP_OFF | CH_BP_ON
		
		"""
		
		c_server_set_rx_filter_run(channel, run)
	
	def set_rx_filter_freq(self, channel, low, high):
		
		"""
		Set the receiver bandpass frequencies on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			low  	-- low cutoff in Hz
			high	-- high cutoff in Hz
			
		Note: frequencies can be +ve or _ve but low masy be numerically less that high
		
		"""
		
		c_server_set_rx_filter_freq(channel, low, high)
	
	def set_rx_filter_window(self, channel, window):
		
		"""
		Set the receiver bandpass window on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			window  -- CH_BP_WIN_4_TERM | CH_BP_WIN_7_TERM
			
		Note: Window is Blackman-Harris 4 or 7 term.
		
		"""
		
		c_server_set_rx_filter_window(channel, window)
	
	def set_agc_mode(self, channel, mode):
		
		"""
		Set the receiver bandpass window on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			mode  -- CH_AGC_OFF | CH_AGC_LONG | CH_AGC_SLOW | CH_AGC_MED | CH_AGC_FAST
			
		Note: All defaults apply at this time.
		
		"""
		
		c_server_set_agc_mode(channel, mode)

	def get_rx_meter_data(self, channel, which):
		
		"""
		Get the requested rx meter data for the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			which  -- M_RX_S_PEAK | M_RX_S_AV | M_RX_IN_PEAK | M_RX_IN_AV | M_RX_AGC_GAIN | M_RX_AGC_PEAK | M_RX_AGC_AV
			
		Returns: reading in db
		
		"""
		
		return c_server_get_rx_meter_data(channel, which)
	
	def set_rx_gain(self, rx, gain):
		"""
		Set the rx output gain
		
		Arguments:
			rx		-- the receiver to set the gain for
			gain  	-- the gain value 0.0 - 1.0
			
		"""
		
		c_server_set_rx_gain(rx, gain)

	#======================================================
	# Transmitter type channel parameters
	def set_tx_mode(self, channel, mode):
		
		"""
		Set the transmitter mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			mode 	-- CH_LSB | CH_USB |CH_DSB |CH_CWL |CH_CWU |CH_FM |CH_AM |CH_DIGU |CH_SPEC |CH_DIGL |CH_SAM | CH_DRM
		
		"""
		
		c_server_set_tx_mode(channel, mode)
	
	def set_tx_filter_run(self, channel, run):
		
		"""
		Set the transmitter bandpass run mode on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			run  	-- CH_BP_OFF | CH_BP_ON
		
		"""
		
		c_server_set_tx_filter_run(channel, run)
	
	def set_tx_filter_freq(self, channel, low, high):
		
		"""
		Set the transmitter bandpass frequencies on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			low  	-- low cutoff in Hz
			high	-- high cutoff in Hz
			
		Note: frequencies can be +ve or _ve but low masy be numerically less that high
		
		"""
		
		c_server_set_tx_filter_freq(channel, low, high)
	
	def set_tx_filter_window(self, channel, window):
		
		"""
		Set the transmitter bandpass window on the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			window  -- CH_BP_WIN_4_TERM | CH_BP_WIN_7_TERM
			
		Note: Window is Blackman-Harris 4 or 7 term.
		
		"""
		
		c_server_set_tx_filter_window(channel, window)
	
	def get_tx_meter_data(self, channel, which):
		
		"""
		Get the requested rx meter data for the given channel
		
		Arguments:
			channel	-- the channel id as returned by open_channel()
			which  -- CH_TX = M_TX_MIC_PK | M_TX_MIV_AV | M_TX_EQ_PK | M_TX_EQ_AV | M_TX_LEV_PK | M_TX_LEV_AV | M_TX_LEV_GAIN | M_TX_COMP_PK | M_TX_COMP_AV | M_TX_ALC_PK | M_TX_ALC_AV | M_TX_ALC_GAIN | M_TX_OUT_PK | M_TX_OUT_AV
			
		Returns: reading in db
		
		"""
		
		return c_server_get_tx_meter_data(channel, which)
	
	def set_tx_gain(self, gain):
		"""
		Set the tx output gain
		
		Arguments:
			gain  	-- the gain value 0.0 - 1.0
			
		"""
		
		c_server_set_tx_gain(gain)
		
	#======================================================
	# Ancilliary functions
	def set_rx_gain(self, rx, gain):
		"""
		Set the rx output gain
		
		Arguments:
			rx		-- the receiver to set the gain for
			gain  	-- the gain value 0.0 - 1.0
			
		"""
		
		c_server_set_rx_gain(rx, gain)
	
	#=========================================================================================================	
	# Display
	
	#======================================================
	# Basic display operations
	def open_display(self, fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, int sample_rate, period, callback):
		"""
		Open a new display unit 
		
		Arguments:
			fft_size		-- fft size to use, power of 2
			win_type		-- window type
			sub_spans		-- number of receivers to stitch
			in_sz			-- number of inpit samples
			display_width	-- number of points to plot, generally pixel width
			average_mode	-- modes available :
								-1	Peak detect
								0	No averaging
								1	Time weighted linear
								2	Time weighted log
								3	Window averaging linear
								4	Window averaging log
								5	Weighted averaging linear, low noise floor mode
								6	Weighted averaging log, low noise floor mode
			over_frames		--	number of frames to average over
			period			--	minimum period in ms between refreah callbacks
			callback		--	callback here with display data every period ms
			sample_rate		--	in Hz
			
		Returns:
			the assigned display number
		
		We start a thread here that collects the display data every period ms for the display unit
		and passes the data to the callback function.
		
		"""
		
		# Open the display unit
		self.__frame_rate = 1000/period
		display_id = c_server_open_display(fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, sample_rate, self.__frame_rate)
		# Create a poller thread and start it running
		o = DisplayPoller(display_id, display_width, period, callback)
		self.__display_poller_list[display_id] = o
		o.start()
		
		return display_id		

	def set_display(self, display_id, fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, int sample_rate):
		"""
		Change display unit parameters 
		
		Arguments:
			display_id		-- change params for this id
			fft_size		-- fft size to use, power of 2
			win_type		-- window type
			sub_spans		-- number of receivers to stitch
			in_sz			-- number of inpit samples
			display_width	-- number of points to plot, generally pixel width
			average_mode	-- modes available :
								-1	Peak detect
								0	No averaging
								1	Time weighted linear
								2	Time weighted log
								3	Window averaging linear
								4	Window averaging log
								5	Weighted averaging linear, low noise floor mode
								6	Weighted averaging log, low noise floor mode
			over_frames		--	number of frames to average over
			sample_rate		--	in Hz
		
		"""
		
		# Set the display unit parameters
		c_server_set_display(display_id, fft_size, win_type, sub_spans, in_sz, display_width, average_mode, over_frames, sample_rate, self.__frame_rate)
		
	def close_display(self, display_id):
		"""
		Close an open display unit 
		
		Arguments:
			display_id 	-- the unit id as returned by open_display()
			
		"""
		
		if display_id in self.__display_poller_list:
			self.__display_poller_list[display_id].terminate()
			self.__display_poller_list[display_id].join()
			del self.__display_poller_list[display_id]
		c_server_close_display(display_id)
	
	# PRIVATE ==========================================================================
	def __init_cc(self):
		"""
		Initialise the cc bytes
		
		"""
		
		# Set the byte 0 values for each sequence
		self.__cc[CC_OUT_GEN][0] = CC_OUT_GEN_SET
		self.__cc[CC_OUT_NCO_1][0] = CC_OUT_NCO_1_SET
		self.__cc[CC_OUT_NCO_2][0] = CC_OUT_NCO_2_SET
		self.__cc[CC_OUT_NCO_3][0] = CC_OUT_NCO_3_SET
		self.__cc[CC_OUT_NCO_4][0] = CC_OUT_NCO_4_SET
		self.__cc[CC_OUT_NCO_5][0] = CC_OUT_NCO_5_SET
		self.__cc[CC_OUT_NCO_6][0] = CC_OUT_NCO_6_SET
		self.__cc[CC_OUT_NCO_7][0] = CC_OUT_NCO_7_SET
		self.__cc[CC_OUT_NCO_8][0] = CC_OUT_NCO_8_SET
		self.__cc[CC_OUT_AUX_1][0] = CC_OUT_AUX_1_SET
		self.__cc[CC_OUT_AUX_2][0] = CC_OUT_AUX_2_SET
		
		# Set reasonable defaults.
		self.set_speed(48000)
		self.set_10MHz_ref('mercury')
		self.set_122_88MHz_ref('mercury')
		self.set_board_config('both')
		self.set_mic_source('penelope')
		self.set_mic_boost(True)
		self.set_mic_line('mic')
		self.set_class_mode('mode_e')
		self.set_oc_outputs(0x00)
		self.set_alex_attn(0)
		self.set_preamp(False)
		self.set_LT2208_dither(False)
		self.set_LT2208_random(False)
		self.set_alex_rx_ant('none')
		self.set_alex_rx_out(False)
		self.set_alex_tx_relay('tx1')
		self.set_duplex(False)
		self.set_number_rx(1)
		self.set_mic_ts(False)
		self.set_mercury_freq_plan('independent')
		for rx in range(1,8):
			self.set_rx_freq(rx, 7.1)
		self.set_tx_freq(rx, 7.1)
		
	def __set_bits(self, value, seq, offset, mask, lookup):
		
		"""
		Set bits in a cc byte
		
		Arguments:
			value	--	the user defined value
			seq		--	the cc sequence to apply to	
			offset	--	the offset in the selected sequence
			mask	--	the mask to clear the bits
			lookup	--	the value to bits lookup table
		
		"""
		
		if isinstance(value, str):
			value = value.lower()
		if value in lookup:
			lock.acquire()
			self.__cc[seq][offset] = (self.__cc[seq][offset] & mask) | lookup[value]
			lock.release()

"""
Display poller class.
"""
class DisplayPoller(threading.Thread):
	
	def __init__(self, display_id, display_sz, period, callback):
		"""
		Check if display data is available for the given display_id
		
		Arguments:
			display_id	--	display unit to call
			display_sz	--	size of return data
			period		-- 	poll period in ms
			callback	--	callback here with data
		
		"""
	
		threading.Thread.__init__(self)
		
		self.__display_id = display_id
		self.__period = period
		self.__callback = callback
		
		self.__terminate = False
		self.__display_data = np.zeros(1920, dtype=np.float)
	
	def terminate(self):		
		""" Set termination """
		
		self.__terminate = True
		
	def run(self):		
		""" Run until termination """

		cdef int flag
		
		while not self.__terminate:
			flag = c_server_get_display_data(self.__display_id, np.PyArray_DATA(self.__display_data))
			if flag == 1:
				self.__callback(self.__display_data)
			if self.__terminate: break
			sleep(float(self.__period)/1000.0)
			
#=======================================================================================	
# These are functions with C types. They can be called internally in Cython but cannot be exposed.
cdef __convert_device_list(DeviceEnumList* devs):
	""" Convert from DeviceEnumList to Python dict """
	
	device_list = []
	for n in range(devs.entries):
		if devs.devices[n].name.decode("ascii").lower().find('sound mapper') == -1:
			device_list.append({'index':devs.devices[n].index, 'name':devs.devices[n].name.decode("ascii"), 'ch':devs.devices[n].channels, 'hostapi':devs.devices[n].host_api.decode("ascii")})
	return device_list

cdef public void message_callback(char* src, char * msg):
	"""
	Callback from C to output a message
	
	Arguments:
		src	--	message source
		msg	--	the message to output
		
	"""
		
	message_q.put ((BOTH, logging.INFO, src, msg))